using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.ComponentModel;
using BassMati.Utilities;


namespace BassMati.Server
{
	/// <summary>
	/// Provides a simple implementation of a socket server.
	/// </summary>
	public class Server
	{
		public Server ( )
		{ 
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.ctor() enter", "Info (Server.exe)" );
			Trace.Indent ( );
			this.clients = new System.Collections.Hashtable( 5 );
			Trace.Unindent ( );
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.ctor() exit", "Info (Server.exe)" );
		}

		protected System.Diagnostics.TraceSwitch traceLevel = new TraceSwitch( "ServerTraceLevel", "Controls trace information output level." );
		protected int clientId;
		protected System.Threading.Thread thread;
		protected TcpListener listener;
		protected System.Collections.Hashtable clients;
		protected bool serverInitiatedDisconnect;
		protected int portNumber = 8080;
		protected string hostAddress = "127.0.0.1";

		#region - Client Connection Events -

		/// <summary>
		/// Defines the event argument of a connection event handler.
		/// </summary>
		public struct ClientConnectionEventArgs
		{
			public int ClientID;
			public int ClientsTotal;
			public ClientConnectionEventArgs ( int clientID, int clientsTotal ) { this.ClientID = clientID; this.ClientsTotal = clientsTotal; }
		}

		/// <summary>
		/// Defines the signature of a connection event handler.
		/// </summary>
		public delegate void ClientConnectionEventHandler ( object sender, ClientConnectionEventArgs e );

		/// <summary>
		/// Fires when a client connects to the server.
		/// </summary>
		public event ClientConnectionEventHandler ClientConnected;
		
		/// <summary>
		/// Fires when a client disconnects from the server.
		/// </summary>
		public event ClientConnectionEventHandler ClientDisconnected;
		
		/// <summary>
		/// Fires the ClientConnected event.
		/// </summary>
		/// <param name="client"></param>
		protected void OnClientConnected( ClientConnection client )
		{
			Event.Fire( this.ClientConnected, new object [ ] { this, new ClientConnectionEventArgs( client.ID, this.clients.Count ) } );
		}
		
		/// <summary>
		/// Fires the ClientDisconnected event.
		/// </summary>
		/// <param name="client"></param>
		protected void OnClientDisconnected( ClientConnection client )
		{
			Event.Fire( this.ClientDisconnected, new object [ ] { this, new ClientConnectionEventArgs( client.ID, this.clients.Count ) } );
		}

		#endregion - Client Connection Events -
	
		public void Start ( )
		{
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.Start() enter", "Info (Server.exe)" );
			Trace.Indent ( );
			try
			{
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Starting server.", "Info (Server.exe)" );
				if ( this.thread != null 
					&& this.thread.ThreadState != System.Threading.ThreadState.Stopped  
					&& this.thread.ThreadState != System.Threading.ThreadState.Aborted 
					)
					throw new System.Exception ( "Thread is already running." );
				this.thread = new Thread( new ThreadStart( this.Work ));
				this.thread.Name = "BassMati Server Thread";
				IPAddress localAddr = IPAddress.Parse ( this.hostAddress );
				this.listener = new TcpListener ( localAddr, this.portNumber );
				Trace.WriteLineIf ( traceLevel.TraceInfo, string.Format( "Listening on {0}:{1}.", this.hostAddress, this.portNumber ), "Info (Server.exe)" );
				this.thread.Start( );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Server started.", "Info (Server.exe)" );
			}
			catch ( Exception ex )
			{
				Trace.WriteLine ( TraceFormatter.FormatException( ex), "Error (Server.exe)" );
				throw ( ex );
			}
			finally
			{
				Trace.Unindent ( );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.Start() exit", "Info (Server.exe)" );
			}
		}

		public void Stop ( )
		{
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.Stop() enter", "Info (Server.exe)" );
			Trace.Indent ( );
			try
			{
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Stopping clients.", "Info (Server.exe)" );
				this.serverInitiatedDisconnect = true;
				lock( this.clients.SyncRoot )
				{
					foreach ( ClientConnection client in this.clients.Values ) client.Stop( );
				}
				serverInitiatedDisconnect = false;
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Clients stopped.", "Info (Server.exe)" );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Stopping server.", "Info (Server.exe)" );
				this.listener.Stop( );
				bool success = this.thread.Join( 10000 );
				if ( ! success )
				{
					Trace.WriteLineIf ( traceLevel.TraceInfo, "Aborting server thread.", "Info (Server.exe)" );
					this.thread.Abort( );
					success = this.thread.Join( 10000 );
				}
				Trace.WriteLineIf ( traceLevel.TraceInfo, string.Format( "Server shutdown {0}.", success ? "succeeded" : "failed" ), "Info (Server.exe)" );
			}
			catch ( Exception ex )
			{
				Trace.WriteLine ( TraceFormatter.FormatException( ex), "Error (Server.exe)" );
				throw ( ex );
			}
			finally
			{
				Trace.Unindent ( );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.Stop() exit", "Info (Server.exe)" );
			}
		}

		protected void Work ( )
		{
			Trace.WriteLineIf ( traceLevel.TraceInfo, string.Format( "Server.Work() enter\r\n  Running on thread {0} - {1}", AppDomain.GetCurrentThreadId( ), Thread.CurrentThread.Name ), "Info (Server.exe)" );
			try
			{
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Starting listener.", "Info (Server.exe)" );
				this.listener.Start ( );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Listener started.", "Info (Server.exe)" );
				while ( true ) 
				{
					Trace.WriteLineIf ( traceLevel.TraceInfo, "Waiting for a connection.", "Info (Server.exe)" );
					TcpClient clientConnection = this.listener.AcceptTcpClient ( );            
					Trace.WriteLineIf ( traceLevel.TraceInfo, "Client connecting.", "Info (Server.exe)" );
					ClientConnection newClient = new ClientConnection ( this, ++clientId, clientConnection );
					lock ( this.clients.SyncRoot ) this.clients.Add ( newClient.ID, newClient );
					newClient.Disconnecting += new BassMati.Server.ClientConnection.DisconnectingEventHandler( client_Disconnecting );
					newClient.Disconnected += new BassMati.Server.ClientConnection.DisconnectedEventHandler(client_Disconnected);
					newClient.Start ( );
					Trace.WriteLineIf ( traceLevel.TraceInfo, "Client connected.", "Info (Server.exe)" );
					this.OnClientConnected( newClient );
				}
			}
			catch ( Exception ex )
			{
				Trace.WriteLine ( TraceFormatter.FormatException( ex), "Error (Server.exe)" );
				// TODO (usp) Throw an exception on the GUI thread.
				// throw ( ex );
			}
			finally
			{
				Trace.Unindent ( );
				Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.Work() exit", "Info (Server.exe)" );
			}
		}

		/// <summary>
		/// Handles the client Disconnecting event.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		/// <remarks>
		/// The reason for dicsonnect can be read from the event args.
		/// </remarks>
		/// 
		private void client_Disconnecting ( object sender, BassMati.Server.ClientConnection.DisconnectingEventArgs e )
		{
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.client_Disconnecting() enter", "Info (Server.exe)" );
			Trace.Indent ( );
			((ClientConnection) sender).Disconnecting -= new ClientConnection.DisconnectingEventHandler( client_Disconnecting );
			this.OnClientDisconnected( (ClientConnection) sender );
			Trace.Unindent ( );
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.client_Disconnecting() exit", "Info (Server.exe)" );
		}

		private void client_Disconnected ( object sender, BassMati.Server.ClientConnection.DisconnectedEventArgs e )
		{
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.client_Disconnected() enter", "Info (Server.exe)" );
			Trace.Indent ( );
			lock ( this.clients.SyncRoot )
			{
				if ( ! this.serverInitiatedDisconnect ) this.clients.Remove( e.ClientID );
			}
			((ClientConnection) sender).Disconnecting -= new ClientConnection.DisconnectingEventHandler( client_Disconnecting );
			((ClientConnection) sender).Disconnected -= new BassMati.Server.ClientConnection.DisconnectedEventHandler ( client_Disconnected );
			this.OnClientDisconnected( (ClientConnection) sender );
			Trace.Unindent ( );
			Trace.WriteLineIf ( traceLevel.TraceInfo, "Server.client_Disconnecting() exit", "Info (Server.exe)" );
		}
	}
}
